// =======================================================================
// 
//       Filename:  parser.cpp
// 
//    Description:  
// 
//        Version:  1.0
//        Created:  26.03.2010 19:52:09
//       Revision:  none
//       Compiler:  g++
// 
//         Author:  Dominik 'domachine' Burgdörfer (-), dominik.burgdoerfer@googlemail.com
//        Company:  -
// 
// =======================================================================

#include "parser.hpp"
#include "utils/citerator.hpp"
#include "utils/regexitermatch.hpp"
#include "exceptions/syntaxerror.hpp"
#include "exceptions/fileerror.hpp"
#include "elements/command.hpp"
#include "elements/text.hpp"
#include "elements/script.hpp"
#include "elements/variable.hpp"
#include "types/float.hpp"
#include "types/integer.hpp"
#include <boost/regex.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/filesystem.hpp>
#include <iostream>
#include <fstream>

using namespace std;
using namespace boost;
using namespace webplodder::elements;
using webplodder::exceptions::SyntaxError;
using webplodder::exceptions::FileError;
namespace fs = boost::filesystem;

namespace webplodder {
    Parser::Parser()
        : m_line(0)
    {

    }

    void Parser::parse(ElementStack& elementStack, istream* in)
    {
        ifstream input;

        if(in == 0) {
            input.open(elementStack.sourcePath().string().c_str());
            mIterator.setStream(input);
        }
        else
            mIterator.setStream(*in);

        m_line = 1;
        m_lineOffset = elementStack.lineOffset();

        m_filePath = elementStack.sourcePath();

        bool textBlock = false;
        Text* textElement = 0;

        for(; mIterator.good(); mIterator.moveForward()) {
            if(mIterator.currentChar() != '{') {
                if(!textBlock) {
                    textElement = new Text(
                            elementStack,
                            mIterator.position(), -1);

                    textBlock = true;
                }

                if(mIterator.currentChar() == '\n')
                    ++m_line;
            }
            else {
                if(mIterator.nextChar() == '{') {
                    safelyIncr();
                    continue;
                }

                if(textBlock) {
                    textElement->setEnd(mIterator.position());
                    elementStack.push_back(
                            shared_ptr<Text>(
                                textElement));
                    textBlock = false;
                }

                elementStack.push_back(parseCmd(elementStack));
            }
        }

        if(textElement != 0 && textElement->end() == -1) {
            textElement->setEnd(mIterator.position());
            elementStack.push_back(
                    shared_ptr<Text>(
                        textElement));
        }
    }

    int Parser::line() const
    {
        return m_line + m_lineOffset;
    }

    void Parser::safelyIncr()
    {
        mIterator.moveForward();

        if(!mIterator.good())
            throw SyntaxError("unexpected end-of-stream.",
                    line(), m_filePath);
    }

    void Parser::skipWhiteSpace()
    {
        regex spaceregex("[ \t\n]");

        while(utils::regexIterMatch(mIterator, spaceregex)) {
            if(mIterator.currentChar() == '\n')
                ++m_line;

            safelyIncr();
        }
    }

    shared_ptr<Script> Parser::parseScript(
            ElementStack& elementStack)
    {
        regex scriptregex("[a-z0-9\\-_A-Z]");
        string scriptSpec;
        shared_ptr<Script> scriptElement( 
                new Script(elementStack));
        bool sourceScript = false;

        safelyIncr();

        skipWhiteSpace();
        if(mIterator.currentChar() == ':') {
            sourceScript = true;
            safelyIncr();
        }
        skipWhiteSpace();

        while(utils::regexIterMatch(mIterator, scriptregex)) {
            scriptSpec.push_back(mIterator.currentChar());
            safelyIncr();
        }

        if(scriptSpec.size() == 0)
            throw SyntaxError("no or invalid script specification",
                    line(), m_filePath);

        scriptElement->setLine(line());

        skipWhiteSpace();
        if(sourceScript) {
            string pathRaw = static_cast<string>(
                    *parseString());
            fs::path path(pathRaw);
            scriptElement->setExternalFile(path);
        }

        if(!sourceScript)
            scriptElement->setBegin(mIterator.position());

        while(true) {
            if(mIterator.currentChar() == '!' && 
                    mIterator.nextChar() == '}') {

                if(!sourceScript)
                    scriptElement->setEnd(mIterator.position());

                safelyIncr();

                break;
            }
            else if(!sourceScript) {
                char cChar = mIterator.currentChar();
                if(cChar == '\n')
                    m_line++;
            }
            safelyIncr();
        }
        
        scriptElement->setSpecification(scriptSpec);

        return scriptElement;
    }

    shared_ptr<Variable> Parser::parseVar(ElementStack& elementStack)
    {
        string cmdName;
        regex cmdregex("[a-zA-Z_0-9]");

        safelyIncr();
        skipWhiteSpace();
        while(utils::regexIterMatch(mIterator, cmdregex)) {
            cmdName.push_back(mIterator.currentChar());
            safelyIncr();
        }

        shared_ptr<Variable> varElement(
                new Variable(elementStack));
        varElement->setName(cmdName);
        varElement->setLine(line());

        skipWhiteSpace();

        if(mIterator.currentChar() != '}')
            throw SyntaxError(
                    "unexpected char, expected '}'",
                    line(), m_filePath);
        return varElement;
    }

    shared_ptr<Element> 
        Parser::parseCmd(ElementStack& elementStack) 
        {
            string cmdName;
            int cmdLine;
            bool braceOpen = false;
            regex cmdregex("[a-zA-Z_0-9]");

            braceOpen = true;
            safelyIncr();

            skipWhiteSpace();

            if(mIterator.currentChar() == '!') {
                return parseScript(elementStack);
            }
            else if(mIterator.currentChar() == '$') {
                return parseVar(elementStack);
            }
            else if(mIterator.currentChar() == ':') {
                safelyIncr();
                skipWhiteSpace();

                string pathRaw = static_cast<string>(
                        *parseString());
                fs::path path(pathRaw);

                safelyIncr();

                skipWhiteSpace();
                if(mIterator.currentChar() != '}')
                    throw SyntaxError("unexpected char, expected '}'", line(),
                            m_filePath);

                shared_ptr<Text> text(new Text(elementStack));
                text->setExternalFile(path);
                text->setLine(line());

                return text;
            }
            else if(mIterator.currentChar() == '#') {
                safelyIncr();
                skipWhiteSpace();

                string pathRaw = static_cast<string>(
                        *parseString());
                fs::path path(pathRaw);

                safelyIncr();

                skipWhiteSpace();
                if(mIterator.currentChar() != '}')
                    throw SyntaxError("unexpected char, expected '}'", line(),
                            m_filePath);

                shared_ptr<ElementStack> stack(new ElementStack(path));
                stack->setLine(line());

                parse(*stack);
                return stack;
            }

            while(utils::regexIterMatch(mIterator, cmdregex)) {
                cmdName.push_back(mIterator.currentChar());
                safelyIncr();
            }

            if(cmdName.size() == 0)
                throw SyntaxError("no or no valid command name",
                        line(), m_filePath);

            cmdLine = line();

            skipWhiteSpace();

            vector<shared_ptr<types::Object> > args;

            if(mIterator.currentChar() == ':') {
                safelyIncr();

                skipWhiteSpace();

                string currentArg;
                while(true) {
                    if(mIterator.currentChar() == '"' ||
                            mIterator.currentChar() == '\'') {
                        args.push_back(parseString());
                        safelyIncr();
                        skipWhiteSpace();
                    }
                    else if(mIterator.currentChar() == '{') {
                        args.push_back(parseCmd(
                                    elementStack));

                        safelyIncr();
                        skipWhiteSpace();
                    }
                    else if(mIterator.currentChar() == '}')
                        break;
                    else if(mIterator.currentChar() == ',') {
                        safelyIncr();
                        skipWhiteSpace();
                    }
                    else {
                        args.push_back(parseNumber());
                        skipWhiteSpace();
                    }
                }
            }
            else if(mIterator.currentChar() != '}') {
                throw SyntaxError(
                        "unexpected char, expected '}' or ':'",
                        line(), m_filePath);
            }

            shared_ptr<Command> element(
                    new Command(elementStack, 
                        cmdName, args));

            element->setLine(cmdLine);
            return element;
        }

    shared_ptr<types::String> Parser::parseString() {
        char delimiter;
        shared_ptr<types::String>
            buffer(new types::String(""));

        if(mIterator.currentChar() == '\'')
            delimiter = '\'';
        else if(mIterator.currentChar() == '"')
            delimiter = '"';
        else
            throw SyntaxError("unexpected char, expected ' or \"", line(),
                                m_filePath);

        char charBeforeLast;

        do {
            char currentChar;
            charBeforeLast = mIterator.lastChar();
            safelyIncr();

            if(mIterator.currentChar() == '\\')
                if(mIterator.lastChar() != '\\')
                    continue;

            if(mIterator.currentChar() == delimiter) {
                if(mIterator.lastChar() != '\\' || charBeforeLast == '\\')
                    break;
            }

            if(mIterator.lastChar() == '\\' && charBeforeLast != '\\') {
                if(mIterator.currentChar() == 'n')
                    currentChar = '\n';
                else if(mIterator.currentChar() == 't')
                    currentChar = '\t';
                else {
                    string errMsg("invalid escape sequence: \\");
                    errMsg.push_back(mIterator.currentChar());
                    throw SyntaxError(errMsg,
                            line(), m_filePath);
                }
            }
            else
                currentChar = mIterator.currentChar();

            buffer->push_back(currentChar);

            if(mIterator.currentChar() == '\n') {
                if(mIterator.lastChar() == '\\' &&
                        charBeforeLast != '\\')
                    buffer->erase(buffer->end()-1);
                ++m_line;
            }
        } while(true);

        return buffer;
    }

    shared_ptr<types::Object> Parser::parseNumber() {
        regex numRegex("[0-9\\.\\-]");
        string buffer;

        while(utils::regexIterMatch(mIterator, numRegex)) {
            buffer.push_back(mIterator.currentChar());
            safelyIncr();
        }

        if(buffer.empty())
            throw SyntaxError("unexpected token", 
                    line(), m_filePath);
        else {
            regex floatRegex("\\-?[0-9]+\\.[0-9]+");
            regex intRegex("\\-?[0-9]+");

            if(regex_match(buffer, floatRegex)) {
                double f = atof(buffer.c_str());

                if(f == HUGE_VAL || f == -HUGE_VAL)
                    throw SyntaxError("double value out of range",
                            line(), m_filePath);

                shared_ptr<types::Float> floatObj(
                        new types::Float(f));

                return floatObj;
            }
            else if(regex_match(buffer, intRegex)) {
                int i = atoi(buffer.c_str());

                if(i == INT_MAX || i == INT_MIN)
                    throw SyntaxError("integer value out of range",
                            line(), m_filePath);

                shared_ptr<types::Integer> 
                    integer(new types::Integer(i));
                return integer;
            }
            else 
                throw SyntaxError("unexpected token", 
                        line(), m_filePath);
        }
    }
}
